package com.mybatis.plugin;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import com.mybatis.entity.Page;

@Intercepts({@Signature(args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}, method = "query", type = Executor.class)})
public class PagePlug implements Interceptor {
    private static final String BOUND_SQL = "sqlSource.boundSql.sql";
    public static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    public static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final List<ResultMapping> EMPTY_RESULTMAPPING = new ArrayList(0);
    private static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
    public static final MetaObject NULL_META_OBJECT = MetaObject.forObject(NullObject.class, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
    private static String dialect = null;
    private static boolean offsetAsPageNum = false;
    private static boolean pageSizeZero = false;
    private static boolean rowBoundsWithCount = false;

    private static class NullObject {
        private NullObject() {
        }
    }

    public static MetaObject forObject(Object object) {
        return MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY);
    }

    public static void startPage(int pageNum, int pageSize) {
        startPage(pageNum, pageSize, true);
    }

    public static void startPage(int pageNum, int pageSize, boolean count) {
        System.out.println("======start========" + Thread.currentThread().getId());
        LOCAL_PAGE.set(new Page(pageNum, pageSize, count));
    }

    private Page getPage(RowBounds rowBounds) {
        Page page = LOCAL_PAGE.get();
        LOCAL_PAGE.remove();
        if (page != null) {
            return page;
        }
        if (offsetAsPageNum) {
            return new Page(rowBounds.getOffset(), rowBounds.getLimit(), rowBoundsWithCount);
        }
        return new Page(rowBounds, rowBoundsWithCount);
    }

    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        RowBounds rowBounds = (RowBounds) args[2];
        System.out.println("======get========" + Thread.currentThread().getId());
        if (LOCAL_PAGE.get() == null && rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        }
        args[2] = RowBounds.DEFAULT;
        MappedStatement ms = (MappedStatement) args[0];
        BoundSql boundSql = ms.getBoundSql(args[1]);
        Page page = getPage(rowBounds);
        if (!pageSizeZero || page.getPageSize() != 0) {
            MappedStatement qs = newMappedStatement(ms, new BoundSqlSqlSource(boundSql));
            if (dialect == null) {
                Properties p = qs.getConfiguration().getVariables();
                if (p.get("dialect") == null) {
                    throw new Exception("请配在SqlSessionFactory.configurationProperties.dialect");
                }
                dialect = (String) p.get("dialect");
            }
            args[0] = qs;
            MetaObject msObject = forObject(qs);
            String sql = (String) msObject.getValue(BOUND_SQL);
            if (page.isCount()) {
                msObject.setValue(BOUND_SQL, getCountSql(sql));
                page.setTotal((long) ((Integer) ((List) invocation.proceed()).get(0)).intValue());
                if (page.getTotal() == 0) {
                    return page;
                }
            }
            if (page.getPageSize() <= 0) {
                return page;
            }
            msObject.setValue(BOUND_SQL, getPageSql(sql, page));
            msObject.setValue("resultMaps", ms.getResultMaps());
            page.addAll((List) invocation.proceed());
            return page;
        }
        page.addAll((List) invocation.proceed());
        page.setPageSize(page.size());
        page.setTotal((long) page.size());
        return page;
    }

    private String getCountSql(String sql) {
        return "select count(0) from (" + sql + ") tmp_count";
    }

    private String getPageSql(String sql, Page page) {
        StringBuilder pageSql = new StringBuilder(200);
        if ("mysql".equals(dialect)) {
            pageSql.append("select * from (");
            pageSql.append(sql);
            pageSql.append(") as tmp_page limit " + page.getStartRow() + "," + page.getPageSize());
        } else if ("hsqldb".equals(dialect)) {
            pageSql.append(sql);
            pageSql.append(" LIMIT " + page.getPageSize() + " OFFSET " + page.getStartRow());
        } else if ("oracle".equals(dialect)) {
            pageSql.append("select * from ( select temp.*, rownum row_id from ( ");
            pageSql.append(sql);
            pageSql.append(" ) temp where rownum <= ").append(page.getEndRow());
            pageSql.append(") where row_id > ").append(page.getStartRow());
        }
        return pageSql.toString();
    }

    private class BoundSqlSqlSource implements SqlSource {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql2) {
            this.boundSql = boundSql2;
        }

        public BoundSql getBoundSql(Object parameterObject) {
            return this.boundSql;
        }
    }

    private MappedStatement newMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
        MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), String.valueOf(ms.getId()) + "_PageHelper", newSqlSource, ms.getSqlCommandType());
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        if (!(ms.getKeyProperties() == null || ms.getKeyProperties().length == 0)) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        List<ResultMap> resultMaps = new ArrayList<ResultMap>();
        resultMaps.add(new ResultMap.Builder(ms.getConfiguration(), ms.getId(), Integer.TYPE, EMPTY_RESULTMAPPING).build());
        builder.resultMaps(resultMaps);
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }

    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    public void setProperties(Properties p) {
        dialect = p.getProperty("dialect");
        if (dialect == null || "".equals(dialect)) {
            throw new RuntimeException("Mybatis分页插件PageHelper无法获取dialect参数!");
        }
        String offset = p.getProperty("offsetAsPageNum");
        if (offset != null && "TRUE".equalsIgnoreCase(offset)) {
            offsetAsPageNum = true;
        }
        String withcount = p.getProperty("rowBoundsWithCount");
        if (withcount != null && "TRUE".equalsIgnoreCase(withcount)) {
            rowBoundsWithCount = true;
        }
        String sizeZero = p.getProperty("pageSizeZero");
        if (sizeZero != null && "TRUE".equalsIgnoreCase(sizeZero)) {
            pageSizeZero = true;
        }
    }
}
